#!/usr/bin/env python
# -*- coding: utf-8 -*-
import json
import time
from traceback import format_exc
from httplib import HTTPException
import urllib
import gzip
import StringIO
import requests


from util.logger import error_log, process_log
from util.app_global import headers, opends_api_url_prefix
from util.crypter import DataCrypter


def enum(**enums):
    return type('Enum', (), enums)


def singleton(cls, *args, **kw):
    instances = {}

    def _singleton():
        if cls not in instances:
            instances[cls] = cls(*args, **kw)
        return instances[cls]

    return _singleton


class OpenDSException(Exception):
    pass


class OpenDS:
    """
    OpenDS交互模块
    """

    token = None

    _no_need_token_url = ['api/opends_token/login']

    def __init__(self, token=None, domain=None, username=None, password=None):
        self.url_prefix = opends_api_url_prefix
        #requests.packages.urllib3.disable_warnings()
        if domain and username and password:
            self.token = self.ds_login(domain, username, password)['access_token']
        elif token:
            self.token = token

    def _request(self, url, payload=None, param=None):
        if not payload:
            payload = {}
        if not param:
            param = {}
        param['_t'] = time.time()

        if url not in self._no_need_token_url and self.token and not param.get('access_token'):
            param['access_token'] = self.token

        try_count = 0
        result = {}
        while True:
            try:
                params = u'%s' % urllib.urlencode(param)
                _url = "%s?%s" % (url, params)
                if payload:
                    payload_str = u'%s' % json.dumps(payload)
                    # gzip
                    s = StringIO.StringIO()
                    g = gzip.GzipFile(fileobj=s, mode='w')
                    g.write(payload_str)
                    g.close()
                    res = requests.post(_url, data=s.getvalue(), headers=headers, verify=False)
                else:
                    res = requests.get(_url, headers=headers, verify=False)
                if res.status_code != 200:
                    raise HTTPException(res.status_code, res.text.strip('\n'), url)
                result = json.loads(res.text)
                break
            except IOError, e:
                try_count += 1
                process_log().info(u'can not connect to server, retry ... | reason: %s' % str(e))
                time.sleep(5)
                if try_count == 5:
                    raise e
            except Exception, e:
                raise e

        process_log().info(u'api:%s\t request_id:%s' % ('/'.join(url.split('/')[-2:]), result.get('request_id', '')))

        if result['status'] != '0':
            # process_log().error(u"%s" % result['errstr'])
            raise OpenDSException(result['errstr'])
        return result['result']

    def ds_login(self, domain, username, password):
        url = '%s/opends_token/login' % self.url_prefix
        try:
            password = DataCrypter().decrypt_data(password)
        except Exception:
            pass
        params = {
            'domain': domain,
            'username': username,
            'password': password
        }
        return self._request(url, param=params)

    def ds_create(self, token, name):
        url = '%s/ds/create' % self.url_prefix
        params = {
            'access_token': token,
            'name': name,
            'type': 'opends'
        }
        return self._request(url, param=params)

    def ds_list(self, token):
        url = '%s/ds/list' % self.url_prefix
        params = {
            'access_token': token
        }
        return self._request(url, param=params)

    def ds_delete(self, token, ds_id):
        url = '%s/ds/delete' % self.url_prefix
        params = {
            'access_token': token,
            'ds_id': ds_id
        }
        return self._request(url, param=params)

    def ds_status(self, token, ds_id, status, error_msg=None):
        url = '%s/ds/status' % self.url_prefix
        params = {
            'access_token': token,
            'ds_id': ds_id,
            'status': status,
        }
        if error_msg is not None:
            params['error_msg'] = error_msg
        return self._request(url, param=params)

    def tb_create(self, token, ds_id, name, schema, uniq_key):
        url = '%s/tb/create' % self.url_prefix
        params = {
            'access_token': token,
        }
        data = {
            'ds_id': ds_id,
            'name': name,
            'schema': schema,
            'uniq_key': uniq_key
        }
        return self._request(url, param=params, payload=data)

    def tb_list(self, token, ds_id):
        url = '%s/tb/list' % self.url_prefix
        params = {
            'access_token': token,
            'ds_id': ds_id
        }
        return self._request(url, param=params)

    def tb_info(self, token, tb_id):
        url = '%s/tb/info' % self.url_prefix
        params = {
            'access_token': token,
            'tb_id': tb_id
        }
        return self._request(url, param=params)

    def tb_clean(self, token, tb_id):
        url = '%s/tb/clean' % self.url_prefix
        params = {
            'access_token': token,
            'tb_id': tb_id
        }
        return self._request(url, param=params)

    def tb_write(self, token, tb_id, data):
        url = '%s/tb/write' % self.url_prefix
        params = {
            'access_token': token,
            'tb_id': tb_id
        }
        data = {'data': data}
        return self._request(url, param=params, payload=data)

    def tb_commit(self, token, tb_id):
        url = '%s/tb/commit' % self.url_prefix
        params = {
            'access_token': token,
            'tb_id': tb_id,
            'fast': 0
        }
        return self._request(url, param=params)

    def tb_merge(self, token, tb_id):
        url = '%s/tb/commit' % self.url_prefix
        params = {
            'access_token': token,
            'tb_id': tb_id,
            'fast': 0
        }
        return self._request(url, param=params)

    def tb_update(self, token, tb_ids):
        url = '%s/tb/update' % self.url_prefix
        params = {
            'access_token': token,
            'tb_ids': json.dumps(tb_ids)
        }
        return self._request(url, param=params)

    def tb_insert(self, token, tb_id, fields, data):
        if not data:
            return True
        url = '%s/tb/insert' % self.url_prefix
        params = {
            'access_token': token,
            'tb_id': tb_id,
            'fields': json.dumps(fields)
        }
        return self._request(url, param=params, payload=data)

    def tb_status(self, token, tb_id, status, error_msg):
        url = '%s/tb/status' % self.url_prefix
        params = {
            'access_token': token,
            'tb_id': tb_id,
            'status': status,
        }
        if error_msg is not None:
            params['error_msg'] = error_msg
        return self._request(url, param=params)

    def field_list(self, token, tb_id):
        url = '%s/field/list' % self.url_prefix
        params = {
            'access_token': token,
            'tb_id': tb_id
        }
        return self._request(url, param=params)

    def field_del(self, token, tb_id, name):
        url = '%s/field/delete' % self.url_prefix
        params = {
            'access_token': token,
            'tb_id': tb_id,
            'name': name
        }
        return self._request(url, param=params)

    def field_add(self, token, tb_id, name, type, uniq_index):
        url = '%s/field/add' % self.url_prefix
        params = {
            'access_token': token,
            'tb_id': tb_id,
            'name': name,
            'type': type,
            'uniq_index': uniq_index
        }
        return self._request(url, param=params)

    def field_modify(self, token, tb_id, name, type, uniq_index):
        url = '%s/field/modify' % self.url_prefix
        params = {
            'access_token': token,
            'tb_id': tb_id,
            'name': name,
            'type': type,
            'uniq_index': uniq_index
        }
        return self._request(url, param=params)

    def data_update(self, token, tb_id, fields, data):
        if not tb_id or not fields or not token:
            return False
        url = "%s/data/update" % self.url_prefix
        params = {
            'access_token': token,
            'tb_id': tb_id,
            'fields': json.dumps(fields)
        }

        return self._request(url, param=params, payload=data)

    def data_delete(self, token, tb_id, fields, data):
        if not tb_id or not fields or not token:
            return False
        url = "%s/data/delete" % self.url_prefix
        params = {
            'access_token': token,
            'tb_id': tb_id,
            'fields': json.dumps(fields)
        }

        return self._request(url, param=params, payload=data)

    def data_bulkdelete(self, token, tb_id, where):
        if not tb_id or not where or not token:
            return False
        url = "%s/data/bulkdelete" % self.url_prefix
        params = {
            'access_token': token,
            'tb_id': tb_id,
            'where': where
        }
        return self._request(url, param=params)

    def data_insert(self, token, tb_id, fields, data):
        if not data:
            return True
        url = '%s/data/insert' % self.url_prefix
        params = {
            'access_token': token,
            'tb_id': tb_id,
            'fields': json.dumps(fields)
        }
        return self._request(url, param=params, payload=data)



class Table(object):
    def __init__(self, ds, name, tb_id, schema, uniq_key):
        """
            the table object for bdp
        Args:
            ds (DS):
            name (str):
        """
        if not name:
            raise OpenDSException(u'Table name is required.')
        if not ds and not type(ds) != DS:
            raise OpenDSException(u'Ds is required.')
        self.name = name

        # remove duplicate field(dict) if exist in schema(list)
        self.schema = [dict(n) for n in set([tuple(s.items()) for s in schema])]
        self.uniq_key = uniq_key
        self.ds = ds
        self.tb_id = tb_id
        if tb_id:
            self.modify_schema()
            self.is_new_table = False
        else:
            self.tb_id = self.ds.opends.tb_create(None, self.ds.ds_id, name, self.schema, uniq_key).get('tb_id')
            self.is_new_table = True

    def insert(self, fields, data):
        """
            insert data to a table
        Args:
            fields (list):
            data (list):
        """
        self.ds.opends.tb_insert(None, self.tb_id, fields, data)

    def modify_schema(self):
        """
            modify the table schema, add field or delete failed.
        """
        schema_used = self.ds.opends.field_list(None, self.tb_id)
        schema_used_name = map(lambda field: field.get('name'), schema_used)
        schema_conf_name = map(lambda field: field.get('name'), self.schema)

        _need_add = filter(lambda field: field.get('name') not in schema_used_name, self.schema)
        _need_del = filter(lambda field: field.get('name') not in schema_conf_name, schema_used)

        for fd in _need_del:
            try:
                self.ds.opends.field_del(None, self.tb_id, fd.get('name'))
            except Exception, e:
                error_log().error(u'Delete field `%s` failed and ignored, reason: %s' % (fd.get('name'), e))

        for fd in _need_add:
            try:
                self.ds.opends.field_add(None, self.tb_id, fd.get('name'), fd.get('type'), 0)
            except Exception, e:
                error_log().error(u'Add field `%s` failed and ignored, reason: %s' % (fd.get('name'), e))

    def commit(self):
        self.ds.opends.tb_commit(None, self.tb_id)

    def clean(self):
        if not self.is_new_table:
            self.ds.opends.tb_clean(None, self.tb_id)

    def destroy(self):
        pass

    def fields(self):
        pass


class DS(object):
    def __init__(self, name, token=None, domain=None, username=None, password=None):
        """
            the database object for bdp
        Args:
            name:
            token:
            domain:
            username:
            password:
        """
        if not name:
            raise OpenDSException(u'DS name is required.')
        self.name = name
        self.opends = OpenDS(token, domain, username, password)
        self.ds_id = None
        self.tables = {}
        self.tb_ids = []
        for ds in self.opends.ds_list(None)['data_source']:
            if name == ds['name']:
                self.ds_id = ds['ds_id']
                for table in ds['tables']:
                    self.tables[table[0]] = table[1]
                break
        if not self.ds_id:
            self.ds_id = self.opends.ds_create(None, name)

    def table(self, name, schema, uniq_key=None):
        """
            create a table object by name and schema, create a new table if not exists, or modify the schema if schema
            has been changed.
        Args:
            name (str):
            schema (list):
            uniq_key (list):

        Returns:
            Table
        """
        tb_id = self.tables.get(name)

        table = Table(self, name, tb_id, schema, uniq_key)
        self.tb_ids.append(table.tb_id)
        return table

    def update(self):
        self.opends.tb_update(None, self.tb_ids)
